package com.ease.framework {

import com.ease.util.ObjectFilter;
import com.ease.util.ReflectionUtil;
import com.ease.util.TaggedPropertyInfo;
import com.ease.util.gc.SharedReference;

// TODO (nice to have): provide a way of specifying the proxy id as a public const on
//      the class, the const being annotated as [InstanceId]
/**
 * This class deals with application objects that serve the role of proxies. Proxies are
 * typically used to talk to remote objects and represent the remote service api.
 *
 * <p>ProxyRegistry helps those application classes that depend on proxies to get
 * injected with appropriate proxy objects. This helps avoid use of the locater pattern
 * which would involve explicit invocation of the framework registry, downcasting of the
 * proxy object, etc.
 *
 * <p>Proxies are supposed to be thin and dumb, so that any app logic intended to be
 * carried out before or after a remote method should be in a command or mediator class
 * (except what can be packaged in a generic proxy base class or adapter).
 *
 * <p>ProxyRegistry also serves as a provider for InjectionManager, i.e. provides
 * injection of views and mediators into other objects through the general purpose
 * injection tag [Injectable]. In its role as a provider for the InjectionManager, it
 * does not create any new proxies, but rather provides a lookup of existing objects only.
 *
 * @example A proxy class may look like this:
 *
 * <listing version="3.0">
 * [LifetimePolicy("keepalive")]
 * public class SomeProxy
 * {
 *    public function getSomething( key:T1, callback:Function = null,
 *                                  callContext:Object = null ):void { ... }
 *    public function updateSomething( key:T1, value:T2, callback:Function = null,
 *                                     callContext:Object = null ):void { ... }
 *    // ...
 * }
 * </listing>
 *
 * <p>Note the [LifetimePolicy] tag, which helps the proxy registry decide whether it
 * should keep an instance of this class around for future use, or whether a new instance
 * should be created for each request.
 *
 * <p>If the proxy class also implements an interface, it may look like following:
 *
 * <listing version="3.0">
 * [DefaultImplementation("some.ns.model::SomeProxy")]
 * public interface ISomeProxy
 * {
 *    function getSomething( key:T1, callback:Function = null,
 *                           callContext:Object = null ):void;
 *    // ...
 * }
 * </listing>
 *
 * @see InjectionManager
 */
public class ProxyRegistry implements IObjectDirectory
{
   private static const TAG_INJECTABLEPROXY_NAME:String = "InjectableProxy";
   private static const TAGPARAM_INSTANCEID_KEY:String =
         Constants.TAGPARAM_INSTANCEID_KEY;
   private static const TAGPARAM_POLICY_KEY:String =
         Constants.TAGPARAM_POLICY_KEY;
   private static var _instance:ProxyRegistry;
   private var _proxyCache:FactoryBasedObjectDirectory;

   /**
    * A global instance of this class. This is the instance that would typically be used
    * by an application.
    *
    * @return
    *    An instance of ProxyRegistry for global use.
    */
   public static function get instance():ProxyRegistry {
      if (_instance == null) {
         _instance = new ProxyRegistry( EventBus.instance, InjectionManager.instance );
      }
      return _instance;
   }

   /**
    * Initializes an instance of this class.
    */
   public function ProxyRegistry( eventBus:EventBus,
                                  injectionMgr:InjectionManager = null ) {
      _proxyCache = new FactoryBasedObjectDirectory( eventBus );
      if (injectionMgr == null) {
         injectionMgr = new InjectionManager( eventBus );
      }
      setInjectionMgr(injectionMgr);
      this.injectionMgr.addProvider( this );
   }

   //------------------------------------------------------------------------------------
   // Instance properties
   // The property provider used avoids directly referencing the global instance.
   // This is done to simplify the object graph for Memory Profiler.

   //-------------------------------------------------------
   // Property: injectionMgr

   private var _injectionMgrRef:SharedReference =
         SharedReference.newReference(InjectionManager);

   /**
    * The InjectionManager associated with this registry. This registry is a provider of
    * objects for this injection manager.
    */
   public function get injectionMgr():InjectionManager {
      return _injectionMgrRef.object;
   }

   private function setInjectionMgr(value:InjectionManager):void {
      _injectionMgrRef.object = value;
   }

   /**
    * Adds the given proxy object to the registry, optionally against a string id.
    *
    * @param proxy
    *    The proxy object to be registered with this proxy registry.
    *
    * @param id
    *    An optional string id that identifies the proxy object. If no id is provided, the
    *    proxy's (class or interface) type can be used for identification, e.g. when
    *    looking up the object from the registry.
    */
   public final function addProxy( proxy:Object, id:String = null ):void {
      _proxyCache.addObject( proxy, id );
   }

   /**
    * Removes the given proxy from the registry.
    *
    * @param proxy
    *    The proxy object to remove.
    */
   public final function removeProxy( proxy:Object ):void {
      _proxyCache.removeObject( proxy );
   }

   /**
    * Looks up a proxy object in the registry by the given string id.
    *
    * @param id
    *    The string identifying the object to be looked up. It would have to be the same
    *    id that was provided when originally registering the proxy in order for this to
    *    be a successful lookup.
    */
   public final function getProxy( id:String ):Object {
      var proxy:Object = _proxyCache.getObject( id );
      return proxy;
   }

   /**
    * Looks up a proxy object in the registry by the given object type.
    *
    * @param type
    *    The type can be either the actual class of the object, or a base class or a
    *    compatible interface.
    */
   public final function getProxyByType( type:Class ):Object {
      var proxy:Object = _proxyCache.getObject( null, type );
      return proxy;
   }

   /**
    * The method looks up the existing proxy objects only (i.e., does not create any new
    * ones). Also, only one proxy object is returned at most. The concrete as well as the
    * base and interface types of the objects are considered for a match with the
    * <code>type</code> given by the spec.
    *
    * @see IObjectDirectory#getObjects()
    */
   public final function getObjects( spec:ObjectFilter, callback:Function,
                                     moduleReference:* = null,
                                     currentObjectSet:Array/*?*/ = null ):void {
      if (ReflectionUtil.isValueType( spec.type )) {
         // Proxy objects can't be of value types.
         callback( [] );
         return;
      }
      var objects:Array/*?*/ = _proxyCache.getExistingObjects( spec );

      // Note: It'd be nice to find the intersection between currentObjectSet and the
      // object set computed here, and place the common items at the front of the array
      // returned from here (in the same order as they are in currentObjectSet), but doing
      // so hasn't been necessary so far.

      callback( objects );
   }

   /** @private */
   public final function evaluateMetaFilter( spec:ObjectFilter, object:* ):Boolean {
      var passes:Boolean = _proxyCache.evaluateMetaFilter( spec, object );
      return passes;
   }

   /**
    * @private
    *
    * This method sets the properties in the given object that are labeled with the
    * [InjectableProxy] tag.
    *
    * @param object
    *    The object whose properties are to be set with injectable proxies.
    */
   internal final function setInjectableProxiesOn( object:Object ):void {
      var proxyProps:Array = ReflectionUtil.getAllKeyValuesForTaggedProperties(
            object, TAG_INJECTABLEPROXY_NAME );
      var moduleReference:* = object;
      for each (var pi:TaggedPropertyInfo in proxyProps) {
         var proxy:Object = null;
         var instanceId:String = ReflectionUtil.getKeyedValueFromTagParams(
               pi.tagArgs, TAGPARAM_INSTANCEID_KEY );
         if (instanceId != null) {
            proxy = getProxy( instanceId );
         }
         if (proxy == null) {
            var policy:String = ReflectionUtil.getKeyedValueFromTagParams(
                  pi.tagArgs, TAGPARAM_POLICY_KEY );
            proxy = _proxyCache.getObject( instanceId, pi.propertyType, moduleReference,
                                           policy );
         }
         if (proxy == null) {
            continue;
         }
         object[pi.name] = proxy;
      }
   }
} // class

}